Dieses Notebook ist ein Skript (Drehbuch) zur Vorstellung grundlegender Funktionen von Jupyter, Python, Pandas und matplotlib, um ein Gefühl für die Arbeit mit den Biblotheken zu bekommen. Daher ist das gewählte Beispiel so gewählt, dass wir typische Aufgaben während einer Datenanalyse bearbeiten. Inhaltlich ist diese Analyse allerdings nicht repräsentativ, da sie lediglich einfach Statistiken über ein Git-Repository darstellt.

Jupyter

Zuallerst sehen wir uns Jupyter genauer an. Das hier ist Jupyter, die interaktive Notebook-Umgebung zum Programmieren. Wir sehen hier eine Zelle, in der wir Python-Code eingeben können. Geben wir einfach einmal einen String namens "Hello World" ein. Mit der Tastenkombination Strg + Enter.


In [1]:
"Hello World"


Out[1]:
'Hello World'

Das Ergebnis ist sofort unter der Zelle sichtbar. Legen wir nun eine weitere Zelle an. Dies funktioniert mit dem Drücken der Taste ESC und einem darauffolgendem Buchstaben b. Alternativ können wir am Ende eines Notebooks eine Zelle mit Shift + Enter ausführen und gleich eine neue Zelle erstellen.

Hier sehen wir gleich eine wichtige Eigenheit von Jupyter: Die Unterscheidung zwischen Befehlsmodus (erreichbar über Taste Esc) und dem Eingabemodus (erreichbar über die Taste Enter). Im Befehlsmodus ist die Umrahmung der aktuellen Zelle blau. Im Eingabemodus wird die Umrahmung grün. Gehen wir in den Befehlsmodus und drücken m. Dies ändert den Zelltyp zu einer Markdown-Zelle. Markdown ist eine einfache Markup-Sprache, mit der Text geschrieben und formatiert werden kann. Damit lassen sich unsere durchgeführten Schritte direkt mit dokumentieren.

Python

Sehen wir uns ein paar grundlegende Python-Programmierkonstrukte an, die wir später in der Arbeit mit Pandas benötigen.


In [2]:
"Hello World"


Out[2]:
'Hello World'

In [3]:
text = "Hello World!"
text[0]


Out[3]:
'H'

In [4]:
text[-1]


Out[4]:
'!'

In [5]:
text[2:5]


Out[5]:
'llo'

In [6]:
text[:-1]


Out[6]:
'Hello World'

Die weitere Funktionalität einer Bibliothek können wir erkunden, indem wir die Methoden und Attribute einer Klasse oder eines Objekts ansehen. Dazu schreiben wir in unserem String-Beispiel text. und nutzen die integrierte Autovervollständigung von Jupyter mittels der Tabulatortaste Tab, um zu sehen, welche Methoden uns aktuell verwendetes Objekt bietet. Gehen wir dann mit der Pfeiltaste unten oder drücken z. B. die ersten Buchstaben von upper, drücken Enter und schließend Shift+ Tab, dann erscheint die Signatur des entsprechenden Funktionalität und der Ausschnitt der Hilfedokumentation. Bei zweimaligem Drücken von Shift + Tab erscheint die Hilfe vollständig. Mit dem Aufruf von upper() auf unsere text-Variable können wir unseren Text in Großbuchstaben schreiben lassen.


In [7]:
text.upper


Out[7]:
<function str.upper>

Die interaktive Quellcode-Dokumentation hilft uns auch herauszufinden, welche Argumente wir in einer Methode zusätzlich zu normale Übergabeparametern hinzufügen können.m


In [8]:
text.split(maxsplit=2, sep=" ")


Out[8]:
['Hello', 'World!']

Git-Historienanalyse

In diesem Notebook wollen wir uns die Entwicklungsgeschichte des Spring Framework Beispielprojekts "Spring PetClinic" anhand der Historie des dazugehörigen Git-Repositories ein wenig genauer ansehen.

Das GitHub-Repository https://github.com/spring-projects/spring-petclinic wurde dafür über den Befehl

https://github.com/spring-projects/spring-petclinic.git

auf die lokale Festplatte geklont.

Die für diese Auswertung relevanten Teile der Historie wurde mittels

git log --pretty="%ad,%aN" --no-merges > timestamp_author.csv

exportiert. Dieser Befehl liefert pro Commit des Git-Repositories den Zeitstempel des Commits (%ad) sowie den Namen des Autors (%aN). Die jeweiligen Werte sind kommasepariert. Wir geben zusätzlich mit an, dass wir reine Merge-Commits nicht erhalten wollen (über --no-merges). Das Ergebnis der Ausgabe speichern wir in die Datei timestamp_author.csv.

Pandas

Nun können wir diese Daten mit Hilfe des Datenanalyse-Frameworks Pandas einlesen. Wir importieren dazu pandas mit der gängigen Abkürzung pd mittels der import ... as ... Syntax von Pyhton.


In [9]:
import pandas as pd

Ob das Importieren des Moduls auch wirklich funktioniert hat, können wir prüfen, in dem wir mit dem pd-Modul arbeiten. Dazu hängen wir an die pd-Variable den ? Operator an und führen die Zelle aus. Es erscheint die Dokumentation des Moduls im unteren Bereich des Notebooks. Diesen Bereich können wir durchlesen und mit der Taste ESC auch wieder verschwinden lassen.


In [10]:
pd?

Danach lesen wir die oben beschriebene CSV-Datei timestamp_author.csv ein und speichern das Ergebnis in der Variable git_log. Neben dem Dateinamen müssen wir zusätzlich über das Argument names noch eine Liste an Namen für die Kopfzeile mitgeben, da unsere Git-Log-Datei keine entsprechende Kopfzeile besitzt.

Wir haben nun die Daten in einen DataFrame (so etwas ähnliches wie ein programmierbares Excel-Arbeitsblatt) geladen, der in unserem Fall aus zwei Series (in etwa Spalten) besteht. Auf den DataFrame können wir nun Operationen ausführen. Z. B. können wir uns mittels head() die fünf ersten Einträge anzeigen lassen.


In [11]:
git_log = pd.read_csv(
    "datasets/git_timestamp_author.csv",
    # oder: 
    #"https://pastebin.com/raw/C40C9S82",
    names=['timestamp', 'author'])
git_log.head()


Out[11]:
timestamp author
0 Thu Feb 15 17:44:48 2018 +0100 Antoine Rey
1 Sat Nov 4 11:59:48 2017 +0100 Antoine Rey
2 Thu Feb 22 10:57:12 2018 +0100 Stephane Nicoll
3 Thu Feb 22 10:46:09 2018 +0100 Stephane Nicoll
4 Mon Feb 5 19:19:38 2018 +0100 Ray Tsang

Als nächstes rufen wir info() auf den DataFrame auf, um einige Eckdaten über die eingelesenen Daten zu erhalten.


In [12]:
git_log.info()


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 527 entries, 0 to 526
Data columns (total 2 columns):
timestamp    527 non-null object
author       527 non-null object
dtypes: object(2)
memory usage: 8.3+ KB

Den Zugriff auf die einzelnen Series können wir mittels der Schreibeweise [<spaltenname>] oder (in den meisten Fällen) per direkter Nutzung des Namens der Series erreichen.


In [13]:
git_log.author.head()


Out[13]:
0        Antoine Rey
1        Antoine Rey
2    Stephane Nicoll
3    Stephane Nicoll
4          Ray Tsang
Name: author, dtype: object

Auch auf einer Series selbst können wir verschiedene Operationen ausführen. Z. B. können wir mit value_counts() die in einer Series enthaltenen Werte zählen und nach gleichzeitig nach ihrer Häufigkeit sortieren lassen. Das Ergebnis ist wieder eine Series, diesmal aber mit den zusammengezählten und sortieren Werten. Auf diese Series können wir zusätzlich ein head(10) aufrufen. So erhalten wir eine schnelle Möglichkeit, die TOP-10-Werte einer Series anzeigen zu lassen. Das Ergebnis können wir dann in einer Variable top10 festhalten und ausgeben lassen, in dem wir die Variable in die nächste Zellenzeile schreiben.


In [14]:
top10 = git_log.author.value_counts().head(10)
top10


Out[14]:
Michael Isvy          258
Antoine Rey           100
Keith Donald           35
Costin Leau            28
Dave Syer              25
Stephane Nicoll         8
Dapeng                  6
Thibault Duchateau      5
Gordon Dickens          5
Cyrille Le Clerc        5
Name: author, dtype: int64

Plotten/Visualisierung

Als nächstes wollen wir das Ergebnis visualisieren bzw. plotten. Um die das Plot-Ergebnis der intern verwendeten Plotting-Bibliothek matplotlib direkt im Notebook anzuzeigen, müssen wir Jupyter dies mit dem Magic-Kommando

%matplotlib inline

vor dem Aufruf der plot() Methode mitteilen.

Standardmäßig wird beim Aufruf von plot() auf einen DataFrame oder einer Series ein Liniendiagramm erstellt.


In [15]:
%matplotlib inline
top10.plot()


Out[15]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa934d3f98>

Das macht hier wenig Sinn, weshalb wir mittels einer Untermethode von plot namens bar() ein Balkendiagramm erzeugen lassen.


In [16]:
top10.plot.bar()


Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa92c99898>

Für diese Daten bietet sich auch eine Visualisierung als Tortendiagramm an. Hierfür rufen wir statt bar() die Methode pie() auf.


In [17]:
top10.plot.pie()


Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa935770f0>

Das Diagramm sieht hier jedoch nicht sehr schön aus.

Mit den optionalen Styling-Parametern können wir erreichen, dass wir eine schönere Grafik angezeigt bekommen. Wir verwenden dazu

  • figsize=[7,7] als Größenangabe
  • title="Top 10 Autoren" als Titel
  • labels=None, um die überflüssige Beschriftung nicht anzuzeigen.

In [18]:
top10.plot.pie(
    figsize=[7,7],
    title="Top 10 Autoren",
    label="")


Out[18]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa936d1f60>

Extraktion von Informationen

Nun widmen wir uns den Zeitstempelangaben. Wir wollen anhand dieser ungefähr herausfinden, wo die meisten Entwickler wohnen. Dazu extrahieren wir die Informationen über den Zeitstempel in timestamp in eine neue Spalte/Series mittels der split()-Funktion, welche uns von den str / String-Funktionen einer Series bereitgestellt wird. Die split()-Funktion benötigt als erstes, optionales Argument das Trennzeichen (standardmäßig ist dies das Leerzeichen) zur Trennung des Strings. Zusätzlich geben wir mit expand=True mit, dass wir als Rückgabewert einen DataFrame haben möchten. Damit können wir mit Hilfe der Selektion einer beliebigen Spalte den für uns interessanten Teil des DataFrames auswählen (in unserem Fall [5]).


In [19]:
git_log.timestamp.head()


Out[19]:
0    Thu Feb 15 17:44:48 2018 +0100
1     Sat Nov 4 11:59:48 2017 +0100
2    Thu Feb 22 10:57:12 2018 +0100
3    Thu Feb 22 10:46:09 2018 +0100
4     Mon Feb 5 19:19:38 2018 +0100
Name: timestamp, dtype: object

In [20]:
git_log.timestamp.str.split().str[5].head()


Out[20]:
0    +0100
1    +0100
2    +0100
3    +0100
4    +0100
Name: timestamp, dtype: object

In [21]:
zeitzone = git_log.timestamp.str.split().str[5]
zeitzone.head()


Out[21]:
0    +0100
1    +0100
2    +0100
3    +0100
4    +0100
Name: timestamp, dtype: object

In [22]:
git_log['timezone'] = zeitzone
git_log.head()


Out[22]:
timestamp author timezone
0 Thu Feb 15 17:44:48 2018 +0100 Antoine Rey +0100
1 Sat Nov 4 11:59:48 2017 +0100 Antoine Rey +0100
2 Thu Feb 22 10:57:12 2018 +0100 Stephane Nicoll +0100
3 Thu Feb 22 10:46:09 2018 +0100 Stephane Nicoll +0100
4 Mon Feb 5 19:19:38 2018 +0100 Ray Tsang +0100

Analog zu den TOP 10 Autoren können wir nun die TOP 10 Zeitzonen ausgeben lassen.


In [23]:
git_log.timezone.value_counts().head(10).plot.pie(
    figsize=[7,7],
    title="Top 10 Zeitzonen",
    label="")


Out[23]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa947a6b38>

Arbeiten mit Datumsangaben

Bevor wir in die Welt der Zeitreihenverarbeitung einsteigen können, müssen wir unsere Spalte mit den Datumsangabe zuerst in den passenden Datentyp umwandeln. Zurzeit ist unsere Spalte timestamp noch ein String, also von textueller Natur. Wir können dies sehen, in dem wir uns mittels der Helferfunktion type(<object>) den ersten Eintrag der timestamp-Spalte anzeigen lassen:


In [24]:
type(git_log.timestamp[0])


Out[24]:
str

Pandas konvertiert standardmäßig automatisch die Zeitzonen, damit wir uns um nichts mehr kümmern müssen. In unserem Fall ist das aber schlecht, da wir die jeweilige lokale Zeit eines Commits erhalten wollen. Daher schneiden wir kurzerhand die Angabe über die Zeitzone ab. Mittels der str-Funktion und dem passenden Selektor [:-6] können wir das einfach bewerkstelligen.


In [25]:
zeitstempel = git_log.timestamp.str[:-6]
zeitstempel.head()


Out[25]:
0    Thu Feb 15 17:44:48 2018
1     Sat Nov 4 11:59:48 2017
2    Thu Feb 22 10:57:12 2018
3    Thu Feb 22 10:46:09 2018
4     Mon Feb 5 19:19:38 2018
Name: timestamp, dtype: object

Beim Umwandeln von Datentypen hilft uns Pandas natürlich ebenfalls. Die Funktion pd.to_datetime nimmt als ersten Parameter eine Series mit Datumsangaben entgegen und wandelt diese um. Als Rückgabewert erhalten wir entsprechend eine Series vom Datentype Timestamp. Die Umwandlung funktioniert für die meisten textuellen Datumsangaben auch meistens automagisch, da Pandas mit unterschiedlichesten Datumsformaten umgehen kann.


In [26]:
git_log['timestamp_local'] = pd.to_datetime(git_log.timestamp.str[:-6])
git_log.head()


Out[26]:
timestamp author timezone timestamp_local
0 Thu Feb 15 17:44:48 2018 +0100 Antoine Rey +0100 2018-02-15 17:44:48
1 Sat Nov 4 11:59:48 2017 +0100 Antoine Rey +0100 2017-11-04 11:59:48
2 Thu Feb 22 10:57:12 2018 +0100 Stephane Nicoll +0100 2018-02-22 10:57:12
3 Thu Feb 22 10:46:09 2018 +0100 Stephane Nicoll +0100 2018-02-22 10:46:09
4 Mon Feb 5 19:19:38 2018 +0100 Ray Tsang +0100 2018-02-05 19:19:38

Ob die Umwandlung erfolgreich war, können wir mit einem nochmaligen Aufruf von type() auf den ersten Wert unserer umgewandelten Spalte timestamp_local überprüfen.


In [27]:
type(git_log.timestamp_local[0])


Out[27]:
pandas._libs.tslib.Timestamp

Nun haben wir einen neuen Datentyp Timestamp in der timestamp_local erhalten, der uns Berechnungen mit Zeitangaben erheblich vereinfacht. Z. B. können wir nun mittels eines einfachen >=-Vergleichs herausfinden, welche Commits nach dem 01.02.2018 stattgefunden haben.


In [28]:
(git_log.timestamp_local > "01.02.2018").head()


Out[28]:
0     True
1    False
2     True
3     True
4     True
Name: timestamp_local, dtype: bool

Wir können nun auch auf einzelne Bestandteile der Datumsangaben zugreifen. Dazu verwenden wir das dt-Objekt ("datetime") und können auf dessen Eigenschaften wie etwa hour zurückgreifen.


In [29]:
git_log.timestamp_local.dt.hour.head()


Out[29]:
0    17
1    11
2    10
3    10
4    19
Name: timestamp_local, dtype: int64

Zusammen mit der bereits oben vorgestellten value_counts()-Methode können wir nun wieder Werte zählen lassen. Wichtig ist hier jedoch, dass wir zusätzlich den Parameter sort=False setzen, um die sortierung nach Mengenangaben zu vermeiden.


In [30]:
commits_je_stunde = git_log.timestamp_local.dt.hour.value_counts(sort=False)
commits_je_stunde.head()


Out[30]:
0    2
1    5
2    2
3    2
5    1
Name: timestamp_local, dtype: int64

Das Ergebnis können wir entsprechend mittels eines Balkendiagramms ausgeben und erhalten so eine Übersicht, zu welcher Tageszeit Quellcode committet wird.


In [31]:
commits_je_stunde.plot.bar()


Out[31]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa94772630>

Wir beschriften nun zusätzlich die Grafik. Dazu speichern wir uns das Rückgabeobjekt der bar()-Funktion in der Variable ax. Hierbei handelt es sich um ein Axes-Objekt der darunterliegenden Plotting-Bibliothek matplotlib, durch das wir zusätzliche Eigenschaften des Plots beliebig anpassen können. Wir setzen hier

  • den Titel über set_title(<titelname>)
  • die Beschriftung der X-Achse mit set_xlabel(<x_achsenname>) und
  • die Beschriftung der Y-Achse mit set_ylabel<y_achsenname>)

Als Ergebnis erhalten wir nun ein ausagekräftiges, beschriftetes Balkendiagramm.


In [32]:
ax = commits_je_stunde.plot.bar()
ax.set_title("Commits pro Stunde")
ax.set_xlabel("Tagesstunde")
ax.set_ylabel("Commits")


Out[32]:
Text(0,0.5,'Commits')

Wir können auch nach Wochentagen auswerten. Dazu verwenden wir das weekday-Attribut auf dem DateTime-Attribut dt. Wie üblich, lassen wir hier die Werte über value_counts zählen, lassen die Werte aber nicht der Größe nach sortieren.


In [33]:
commits_je_wochentag = git_log.timestamp_local.dt.weekday.value_counts(sort=False)
commits_je_wochentag


Out[33]:
0     80
1     75
2    104
3     92
4     97
5     51
6     28
Name: timestamp_local, dtype: int64

Das Ergebnis in commits_je_wochentag lassen wir als ein Balkendiagramm mittels plot.bar() ausgeben.


In [34]:
commits_je_wochentag.plot.bar()


Out[34]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa948b9b38>

Commit-Verlauf

Nachfolgend wollen wir den Verlauf aller Commits über die letzten Jahre aufzeichnen lassen. Dazu setzen wir die timestamp Spalte als Index mittels set_index(<spaltenname>). Zudem selektieren wir lediglich die author-Spalte mittels [<spaltenname>]. Dadurch arbeiten wir fortlaufend auf einer reinen Series statt eines DataFrame. Randnotiz: Die Verarbeitung mittels Series folgt fast analog wie bei einem DataFrame. Eine Series wird jedoch nicht so schön in einer Tabelle formatiert angezeigt, weshalb ich persönlich die Bearbeitung mittels DataFrame bevorzuge.


In [35]:
git_timed = git_log.set_index('timestamp_local')['author']
git_timed.head()


Out[35]:
timestamp_local
2018-02-15 17:44:48        Antoine Rey
2017-11-04 11:59:48        Antoine Rey
2018-02-22 10:57:12    Stephane Nicoll
2018-02-22 10:46:09    Stephane Nicoll
2018-02-05 19:19:38          Ray Tsang
Name: author, dtype: object

Über die resample(<zeiteinheit>)-Funktion des DataFrames können wir nun Werte nach bestimmten Zeiteinheiten gruppieren wie z. B. nach Tage (D), Monate (M), Quartale (Q) oder Jahre (A). Wir verwenden hier ein resample("D") für tageweises zählen. Zudem geben wir noch an, wie die Einzelwerte pro Zeiteinheit zusammengeführt werden sollen. Hierzu wählen wir die count()-Funktion, um die Anzahl der Commits für jeden einzelnen Tag zu zählen.


In [36]:
commits_per_day = git_timed.resample("D").count()
commits_per_day.head()


Out[36]:
timestamp_local
2009-05-05     5
2009-05-06    10
2009-05-07     1
2009-05-08     0
2009-05-09     0
Freq: D, Name: author, dtype: int64

Um den Commit-Verlauf über die Jahre hinweg aufzuzeigen, bilden wir die kumulative Summe über alle Tageseinträge mittels cumsum(). Damit werden alle Werte nacheinander aufsummiert.


In [37]:
commits_pro_tag_kumulativ = commits_per_day.cumsum()
commits_pro_tag_kumulativ.head()


Out[37]:
timestamp_local
2009-05-05     5
2009-05-06    15
2009-05-07    16
2009-05-08    16
2009-05-09    16
Freq: D, Name: author, dtype: int64

Das Ergebnis plotten wir nun als Liniendiagramm und erhalten somit die Anzahl der Commits über die Jahre hinweg aufgezeichnet.


In [38]:
commits_pro_tag_kumulativ.plot()


Out[38]:
<matplotlib.axes._subplots.AxesSubplot at 0x2aa949ef588>

Was noch fehlt

Wir haben jetzt einige Grundlagen zu Pandas kennengelernt. Die anderen wichtigen Themenbereiche, die nun noch fehlen, sind:

  • Einlesen komplizierter Datenstrukturen
  • Bereinigung von schlechter Datenqualität
  • Zusammenführen verschiedener Datenquellen
  • Gruppieren von gleichartigen Daten mittels groupby
  • Umformen von DataFrames mittels pivot_table